//! Console input and output, to the uart.
//! Reads are line at a time.
//! Implements special input characters:
//!   newline -- end of line
//!   control-h -- backspace
//!   control-u -- kill line
//!   control-d -- end of file
//!   control-p -- print process list

const std = @import("std");
const uart = @import("uart.zig");
const os = @import("../os.zig");
const proc = os.proc;

const gpa = std.heap.page_allocator;
const backspace = 0x08;
const delete = 0x7f;

var lock: os.Lock.Spin = .init("console");
var ring_bytes: [128]u8 = @splat(0);
var ring: std.Deque(u8) = .initBuffer(&ring_bytes);

comptime {
    @import("../driver.zig").register(&init, @typeName(@This()));
}

fn init(_: ?*const anyopaque) void {
    // connect read and write system calls
    os.fs.devsw[os.CONSOLE].read = &read;
    os.fs.devsw[os.CONSOLE].write = &write;
}

/// Writes data from a buffer to the console output.
fn write(src: usize, len: usize) !usize {
    var stack: [32]u8 = undefined;
    const large = len > stack.len;
    const buffer: []u8 = if (large) try gpa.alloc(u8, os.PGSIZE) else &stack;
    defer if (large) gpa.free(buffer);

    const p = proc.current().?;
    var r = p.reader(.auto, buffer, @ptrFromInt(src));

    var i: usize = 0;
    while (i < len) {
        const bytes = try r.io_r.take(@min(buffer.len, len - i));
        try uart.write(bytes);
        i += bytes.len;
    }
    return len;
}

/// Reads input from the console into a buffer. Copies up to a full line of input to the destination
/// address.
fn read(dst: usize, nbytes: usize) !usize {
    const p = proc.current().?;
    var w = p.writer(.auto, &.{}, @ptrFromInt(dst));

    lock.acquire();
    defer lock.release();

    var n = nbytes;
    while (n > 0) {
        // wait until interrupt handler has put some input into buffer.
        while (ring.len == 0) {
            if (p.isKilled()) return error.ProcessNotFound;
            p.sleep(&ring, &lock);
        }

        if (ring.at(0) == ctrl('D')) { // end-of-file
            // Save ^D for next time, to make sure caller gets a 0-byte result.
            if (n == nbytes) _ = ring.popFront().?;
            break;
        }

        // copy the input byte to the user-space buffer.
        const char = ring.popFront().?;
        try w.io_w.writeByte(char);
        n -= 1;

        // a whole line has arrived, return to the user-level read().
        if (char == '\n') break;
    }

    return nbytes - n;
}

/// the console input interrupt handler. uart.intr() calls this for input character. do erase/kill
/// processing, append to ring buffer, wake up read() if a whole line has arrived.
pub fn intr(char: u8) void {
    lock.acquire();
    defer lock.release();

    switch (char) {
        // Print process list.
        ctrl('P') => proc.dump(),
        // Kill line.
        ctrl('U') => {
            while (ring.len > 0 and ring.at(ring.len - 1) != '\n') {
                _ = ring.popBack().?;
                uart.putc(backspace);
            }
        },
        backspace,
        delete,
        => {
            if (ring.len > 0) {
                _ = ring.popBack().?;
                uart.putc(backspace);
            }
        },
        else => {
            if (char == 0 or ring.len >= ring.buffer.len) return;
            const c: u8 = if (char == '\r') '\n' else char;

            // echo back to the user
            uart.putc(c);

            // store for consumption by read().
            ring.pushBackBounded(c) catch unreachable;

            if (c == '\n' or c == ctrl('D') or ring.len == ring.buffer.len) {
                // wake up read() if a whole line (or end-of-file) has arrived.
                proc.wakeup(&ring);
            }
        },
    }
}

/// Control-x
fn ctrl(comptime x: u8) u8 {
    return x - '@';
}
